/* * Copyright 2014-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.rest.webmvc.json; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import org.springframework.beans.PropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.rest.webmvc.mapping.Associations; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; /** * Component to apply an {@link ObjectNode} to an existing domain object. This is effectively a best-effort workaround * for Jackson's inability to apply a (partial) JSON document to an existing object in a deeply nested way. We manually * detect nested objects, lookup the original value and apply the merge recursively. * * @author Oliver Gierke * @author Mark Paluch * @author Craig Andrews * @author Mathias Düsterhöft * @since 2.2 */ @RequiredArgsConstructor public class DomainObjectReader { private final @NonNull PersistentEntities entities; private final @NonNull Associations associationLinks; /** * Reads the given input stream into an {@link ObjectNode} and applies that to the given existing instance. * * @param source must not be {@literal null}. * @param target must not be {@literal null}. * @param mapper must not be {@literal null}. * @return */ public <T> T read(InputStream source, T target, ObjectMapper mapper) { Assert.notNull(target, "Target object must not be null!"); Assert.notNull(source, "InputStream must not be null!"); Assert.notNull(mapper, "ObjectMapper must not be null!"); try { return doMerge((ObjectNode) mapper.readTree(source), target, mapper); } catch (Exception o_O) { throw new HttpMessageNotReadableException("Could not read payload!", o_O); } } /** * Reads the given source node onto the given target object and applies PUT semantics, i.e. explicitly * * @param source must not be {@literal null}. * @param target must not be {@literal null}. * @param mapper * @return */ @SuppressWarnings("unchecked") public <T> T readPut(final ObjectNode source, T target, final ObjectMapper mapper) { Assert.notNull(source, "ObjectNode must not be null!"); Assert.notNull(target, "Existing object instance must not be null!"); Assert.notNull(mapper, "ObjectMapper must not be null!"); Class<? extends Object> type = target.getClass(); entities.getRequiredPersistentEntity(type); try { Object intermediate = mapper.readerFor(target.getClass()).readValue(source); return (T) mergeForPut(intermediate, target, mapper); } catch (Exception o_O) { throw new HttpMessageNotReadableException("Could not read payload!", o_O); } } /** * Merges the state of given source object onto the target one preserving PUT semantics. * * @param source can be {@literal null}. * @param target can be {@literal null}. * @param mapper must not be {@literal null}. * @return */ <T> T mergeForPut(T source, T target, final ObjectMapper mapper) { Assert.notNull(mapper, "ObjectMapper must not be null!"); if (target == null || source == null) { return source; } Class<? extends Object> type = target.getClass(); return entities.getPersistentEntity(type).map(it -> { MergingPropertyHandler propertyHandler = new MergingPropertyHandler(source, target, it, mapper); it.doWithProperties(propertyHandler); it.doWithAssociations(new LinkedAssociationSkippingAssociationHandler(associationLinks, propertyHandler)); // Need to copy unmapped properties as the PersistentProperty model currently does not contain any transient // properties copyRemainingProperties(propertyHandler.getProperties(), source, target); return target; }).orElse(source); } /** * Copies the unmapped properties of the given {@link MappedProperties} from the source object to the target instance. * * @param properties must not be {@literal null}. * @param source must not be {@literal null}. * @param target must not be {@literal null}. */ private static void copyRemainingProperties(MappedProperties properties, Object source, Object target) { PropertyAccessor sourceFieldAccessor = PropertyAccessorFactory.forDirectFieldAccess(source); PropertyAccessor sourcePropertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(source); PropertyAccessor targetFieldAccessor = PropertyAccessorFactory.forDirectFieldAccess(target); PropertyAccessor targetPropertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(target); for (String property : properties.getSpringDataUnmappedProperties()) { // If there's a field we can just copy it. if (targetFieldAccessor.isWritableProperty(property)) { targetFieldAccessor.setPropertyValue(property, sourceFieldAccessor.getPropertyValue(property)); continue; } // Otherwise only copy if there's both a getter and setter. if (targetPropertyAccessor.isWritableProperty(property) && sourcePropertyAccessor.isReadableProperty(property)) { targetPropertyAccessor.setPropertyValue(property, sourcePropertyAccessor.getPropertyValue(property)); } } } public <T> T merge(ObjectNode source, T target, ObjectMapper mapper) { try { return doMerge(source, target, mapper); } catch (Exception o_O) { throw new HttpMessageNotReadableException("Could not read payload!", o_O); } } /** * Merges the given {@link ObjectNode} onto the given object. * * @param root must not be {@literal null}. * @param target must not be {@literal null}. * @param mapper must not be {@literal null}. * @return * @throws Exception */ @SuppressWarnings("unchecked") <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception { Assert.notNull(root, "Root ObjectNode must not be null!"); Assert.notNull(target, "Target object instance must not be null!"); Assert.notNull(mapper, "ObjectMapper must not be null!"); Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> candidate = entities .getPersistentEntity(target.getClass()); if (!candidate.isPresent()) { return mapper.readerForUpdating(target).readValue(root); } PersistentEntity<?, ?> entity = candidate.get(); MappedProperties mappedProperties = MappedProperties.fromJacksonProperties(entity, mapper); for (Iterator<Entry<String, JsonNode>> i = root.fields(); i.hasNext();) { Entry<String, JsonNode> entry = i.next(); JsonNode child = entry.getValue(); String fieldName = entry.getKey(); if (!mappedProperties.hasPersistentPropertyForField(fieldName)) { continue; } PersistentProperty<?> property = mappedProperties.getPersistentProperty(fieldName); PersistentPropertyAccessor accessor = entity.getPropertyAccessor(target); Optional<Object> rawValue = accessor.getProperty(property); if (!rawValue.isPresent() || associationLinks.isLinkableAssociation(property)) { continue; } rawValue.ifPresent(it -> { if (child.isArray()) { if (handleArray(child, it, mapper, property.getTypeInformation())) { i.remove(); } return; } if (child.isObject()) { ObjectNode objectNode = (ObjectNode) child; if (property.isMap()) { // Keep empty Map to wipe it as expected if (!objectNode.fieldNames().hasNext()) { return; } execute( () -> doMergeNestedMap((Map<Object, Object>) it, objectNode, mapper, property.getTypeInformation())); // Remove potentially emptied Map as values have been handled recursively if (!objectNode.fieldNames().hasNext()) { i.remove(); } return; } if (property.isEntity()) { i.remove(); execute(() -> doMerge(objectNode, it, mapper)); } } }); } return mapper.readerForUpdating(target).readValue(root); } /** * Handles the given {@link JsonNode} by treating it as {@link ArrayNode} and the given source value as * {@link Collection}-like value. Looks up the actual type to handle from the potentially available first element, * falling back to component type lookup on the given type. * * @param node must not be {@literal null}. * @param source must not be {@literal null}. * @param mapper must not be {@literal null}. * @param collectionType must not be {@literal null}. * @return * @throws Exception */ private boolean handleArray(JsonNode node, Object source, ObjectMapper mapper, TypeInformation<?> collectionType) { Collection<Object> collection = ifCollection(source); if (collection == null) { return false; } return execute(() -> handleArrayNode((ArrayNode) node, collection, mapper, collectionType.getComponentType())); } /** * Applies the diff handling to {@link ArrayNode}s, potentially recursing into nested ones. * * @param array the source {@link ArrayNode}m, must not be {@literal null}. * @param collection the actual collection values, must not be {@literal null}. * @param mapper the {@link ObjectMapper} to use, must not be {@literal null}. * @param componentType the item type of the collection, can be {@literal null}. * @return whether an object merge has been applied to the {@link ArrayNode}. */ private boolean handleArrayNode(ArrayNode array, Collection<Object> collection, ObjectMapper mapper, Optional<TypeInformation<?>> componentType) throws Exception { Assert.notNull(array, "ArrayNode must not be null!"); Assert.notNull(collection, "Source collection must not be null!"); Assert.notNull(mapper, "ObjectMapper must not be null!"); // We need an iterator for the original collection. // We might modify it but we want to keep iterating over the original collection. Iterator<Object> value = new ArrayList<Object>(collection).iterator(); boolean nestedObjectFound = false; for (JsonNode jsonNode : array) { if (!value.hasNext()) { collection.add(mapper.treeToValue(jsonNode, getTypeToMap(null, componentType).getType())); continue; } Object next = value.next(); if (ArrayNode.class.isInstance(jsonNode)) { return handleArray(jsonNode, next, mapper, getTypeToMap(value, componentType)); } if (ObjectNode.class.isInstance(jsonNode)) { nestedObjectFound = true; doMerge((ObjectNode) jsonNode, next, mapper); } } // there are more items in the collection than contained in the JSON node - remove it. while (value.hasNext()) { collection.remove(value.next()); } return nestedObjectFound; } /** * Merges nested {@link Map} values for the given source {@link Map}, the {@link ObjectNode} and {@link ObjectMapper}. * * @param source can be {@literal null}. * @param node must not be {@literal null}. * @param mapper must not be {@literal null}. * @throws Exception */ private void doMergeNestedMap(Map<Object, Object> source, ObjectNode node, ObjectMapper mapper, TypeInformation<?> type) throws Exception { if (source == null) { return; } Iterator<Entry<String, JsonNode>> fields = node.fields(); Class<?> keyType = typeOrObject(type.getComponentType()); Optional<TypeInformation<?>> valueType = type.getMapValueType(); while (fields.hasNext()) { Entry<String, JsonNode> entry = fields.next(); JsonNode value = entry.getValue(); String key = entry.getKey(); Object mappedKey = mapper.readValue(quote(key), keyType); Object sourceValue = source.get(mappedKey); TypeInformation<?> typeToMap = getTypeToMap(sourceValue, valueType); if (value instanceof ObjectNode && sourceValue != null) { doMerge((ObjectNode) value, sourceValue, mapper); } else if (value instanceof ArrayNode && sourceValue != null) { handleArray(value, sourceValue, mapper, getTypeToMap(sourceValue, Optional.of(typeToMap))); } else { source.put(mappedKey, mapper.treeToValue(value, typeToMap.getType())); } fields.remove(); } } @SuppressWarnings("unchecked") private Optional<Map<Object, Object>> mergeMaps(PersistentProperty<?> property, Optional<Object> source, Optional<Object> target, ObjectMapper mapper) { return source.map(it -> { Map<Object, Object> sourceMap = (Map<Object, Object>) it; Map<Object, Object> targetMap = (Map<Object, Object>) target.orElse(null); Map<Object, Object> result = targetMap == null ? CollectionFactory.createMap(Map.class, sourceMap.size()) : CollectionFactory.createApproximateMap(targetMap, sourceMap.size()); for (Entry<Object, Object> entry : sourceMap.entrySet()) { Object targetValue = targetMap == null ? null : targetMap.get(entry.getKey()); result.put(entry.getKey(), mergeForPut(entry.getValue(), targetValue, mapper)); } if (targetMap == null) { return result; } try { targetMap.clear(); targetMap.putAll(result); return targetMap; } catch (UnsupportedOperationException o_O) { return result; } }); } private Optional<Collection<Object>> mergeCollections(PersistentProperty<?> property, Optional<Object> source, Optional<Object> target, ObjectMapper mapper) { return source.map(it -> { Collection<Object> sourceCollection = asCollection(it); Collection<Object> targetCollection = asCollection(target.orElse(null)); Collection<Object> result = targetCollection == null ? CollectionFactory.createCollection(Collection.class, sourceCollection.size()) : CollectionFactory.createApproximateCollection(targetCollection, sourceCollection.size()); Iterator<Object> sourceIterator = sourceCollection.iterator(); Iterator<Object> targetIterator = targetCollection == null ? Collections.emptyIterator() : targetCollection.iterator(); while (sourceIterator.hasNext()) { Object sourceElement = sourceIterator.next(); Object targetElement = targetIterator.hasNext() ? targetIterator.next() : null; result.add(mergeForPut(sourceElement, targetElement, mapper)); } if (targetCollection == null) { return result; } try { targetCollection.clear(); targetCollection.addAll(result); return targetCollection; } catch (UnsupportedOperationException o_O) { return result; } }); } @SuppressWarnings("unchecked") private static Collection<Object> asCollection(Object source) { if (source instanceof Collection) { return (Collection<Object>) source; } else if (source.getClass().isArray()) { return Arrays.asList(ObjectUtils.toObjectArray(source)); } else { return Collections.singleton(source); } } /** * Returns the given source instance as {@link Collection} or creates a new one for the given type. * * @param source can be {@literal null}. * @param type must not be {@literal null} in case {@code source} is null. * @return */ @SuppressWarnings("unchecked") private static Collection<Object> ifCollection(Object source) { Assert.notNull(source, "Source instance must not be null!"); if (source instanceof Collection) { return (Collection<Object>) source; } if (source.getClass().isArray()) { return Arrays.asList((Object[]) source); } return null; } /** * Surrounds the given source {@link String} with quotes so that they represent a valid JSON String. * * @param source can be {@literal null}. * @return */ private static String quote(String source) { return source == null ? null : "\"".concat(source).concat("\""); } /** * Returns the raw type of the given {@link TypeInformation} or {@link Object} as fallback. * * @param type can be {@literal null}. * @return */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static Class<?> typeOrObject(Optional<TypeInformation<?>> type) { return type.map(it -> it.getType()).orElse((Class) Object.class); } /** * Returns the type to read for the given value and default type. The type will be defaulted to {@link Object} if * missing. If the given value's type is different from the given default (i.e. more concrete) the value's type will * be used. * * @param value can be {@literal null}. * @param type can be {@literal null}. * @return */ private static TypeInformation<?> getTypeToMap(Object value, Optional<TypeInformation<?>> type) { return type.map(it -> { if (value == null) { return it; } if (Enum.class.isInstance(value)) { return ClassTypeInformation.from(((Enum<?>) value).getDeclaringClass()); } return value.getClass().equals(it.getType()) ? it : ClassTypeInformation.from(value.getClass()); }).orElse(ClassTypeInformation.OBJECT); } /** * {@link SimpleAssociationHandler} that skips linkable associations and forwards handling for all other ones to the * delegate {@link SimplePropertyHandler}. * * @author Oliver Gierke */ @RequiredArgsConstructor private final class LinkedAssociationSkippingAssociationHandler implements SimpleAssociationHandler { private final @NonNull Associations associations; private final @NonNull SimplePropertyHandler delegate; /* * (non-Javadoc) * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) */ @Override public void doWithAssociation(Association<? extends PersistentProperty<?>> association) { if (associationLinks.isLinkableAssociation(association)) { return; } delegate.doWithPersistentProperty(association.getInverse()); } } /** * {@link SimplePropertyHandler} to merge the states of the given objects. * * @author Oliver Gierke */ private class MergingPropertyHandler implements SimplePropertyHandler { private final @Getter MappedProperties properties; private final PersistentPropertyAccessor targetAccessor; private final PersistentPropertyAccessor sourceAccessor; private final ObjectMapper mapper; /** * Creates a new {@link MergingPropertyHandler} for the given source, target, {@link PersistentEntity} and * {@link ObjectMapper}. * * @param source must not be {@literal null}. * @param target must not be {@literal null}. * @param entity must not be {@literal null}. * @param mapper must not be {@literal null}. */ public MergingPropertyHandler(Object source, Object target, PersistentEntity<?, ?> entity, ObjectMapper mapper) { Assert.notNull(source, "Source instance must not be null!"); Assert.notNull(target, "Target instance must not be null!"); Assert.notNull(entity, "PersistentEntity must not be null!"); Assert.notNull(mapper, "ObjectMapper must not be null!"); this.properties = MappedProperties.fromJacksonProperties(entity, mapper); this.targetAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(target), new DefaultConversionService()); this.sourceAccessor = entity.getPropertyAccessor(source); this.mapper = mapper; } /* * (non-Javadoc) * @see org.springframework.data.mapping.SimplePropertyHandler#doWithPersistentProperty(org.springframework.data.mapping.PersistentProperty) */ @Override public void doWithPersistentProperty(PersistentProperty<?> property) { if (property.isIdProperty() || property.isVersionProperty() || !property.isWritable()) { return; } if (!properties.isMappedProperty(property)) { return; } Optional<Object> sourceValue = sourceAccessor.getProperty(property); Optional<Object> targetValue = targetAccessor.getProperty(property); Optional<?> result = Optional.empty(); if (property.isMap()) { result = mergeMaps(property, sourceValue, targetValue, mapper); } else if (property.isCollectionLike()) { result = mergeCollections(property, sourceValue, targetValue, mapper); } else if (property.isEntity()) { result = mergeForPut(sourceValue, targetValue, mapper); } else { result = sourceValue; } targetAccessor.setProperty(property, result); } } private static <T> T execute(SupplierWithException<T> block) { try { return block.execute(); } catch (Exception o_O) { throw new RuntimeException(o_O); } } private static void execute(RunnableWithException block) { try { block.execute(); } catch (Exception o_O) { throw new RuntimeException(o_O); } } interface RunnableWithException { void execute() throws Exception; } interface SupplierWithException<T> { T execute() throws Exception; } }